package com.uprizer.free.paramopt;

import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentSkipListMap;

public class ParameterOptimizer implements Serializable {
	private static final long serialVersionUID = 3977848045089351547L;
	
	public static final double PARAM_OPT_MUTATION = 0.1;
	
	public static final int PARAM_OPT_ITERATIONS = 10000;
	
	private static final int CONVERGE_SPEED = 10;

	public static Random random = new Random();
	
	public ConcurrentSkipListMap<Double, Double> samples = new ConcurrentSkipListMap<Double, Double>();
	double minX, maxX, testRange, lastChange = 1000, previousChange = 1000;
	boolean current = true;
	Curve bestFit;

	/**
	 * Create a ParameterOptomizer, specifying the minimum and maximum reasonable
	 * values for the parameter to be optomized
	 * @param minX
	 * @param maxX
	 */
	public ParameterOptimizer(double minX, double maxX) {
		this(minX, maxX, (minX + maxX) / 2);
	}

	/**
	 * Create a ParameterOptomizer, specifying the minimum and maximum reasonable
	 * values for the parameter to be optomized, and an initial guess as to what
	 * the optimal value might be
	 * @param minX
	 * @param maxX
	 * @param initial
	 */
	public ParameterOptimizer(double minX, double maxX, double initial) {
		this.minX = minX;
		this.maxX = maxX;
		testRange = maxX-minX;
		bestFit = new Curve(0.1, initial, 0.1);
	}

	/**
	 * Get our current best guess as to the optimal value of this parameter
	 */
	public double getBestGuess() {
		fitCurve(PARAM_OPT_MUTATION, PARAM_OPT_ITERATIONS);
		return bestFit.center;
	}

	/**
	 * Retrieve a list of values to try
	 * @param number The number of values to test in parallel, should be at least 3
	 */
	public List<Double> getNextSamples(int number) {
		fitCurve(PARAM_OPT_MUTATION, PARAM_OPT_ITERATIONS);
		if (number < 3) {
			System.err.println("Requesting only " + number
					+ " samples, this won't be sufficient to create a curve");
		}
		if (samples.isEmpty()) {
			return getSpread(minX, maxX, number);
		} else {
			fitCurve(PARAM_OPT_MUTATION, PARAM_OPT_ITERATIONS);
			// If the last change in guess was smaller than the change
			// in guess, reduce the range, otherwise, increase it
			if (lastChange<=previousChange) {
				testRange /= 4.0;
			} else {
				testRange *= 3.0;
			}
			if (testRange > maxX-minX) {
				testRange = maxX-minX;
			}
			double lowBound = bestFit.center - (testRange / 2);
			double highBound = bestFit.center + (testRange / 2);
			if (lowBound < minX)
				lowBound = minX;
			if (highBound > maxX)
				highBound = maxX;
			return getSpread(lowBound, highBound, number);
		}
	}

	private List<Double> getSpread(double start, double end, int num) {
		num --;
		List<Double> ret = new LinkedList<Double>();
		for (int x=0; x<= num; x++) {
			ret.add(start +((end-start)*x)/num);
		}
		return ret;
	}
	
	private void fitCurve(double mutation, int iterations) {
		// Only fit the curve if we've obtained new samples since we last
		// did it
		if (current) return;
		double oldBestGuess = bestFit.center;
		double currentVariance = bestFit.getVariance(samples);
		for (int x = 0; x < iterations; x++) {
			Curve candidate = new Curve(bestFit, mutation, minX, maxX);
			double candidateVariance = candidate.getVariance(samples);
			if (candidateVariance < currentVariance) {
				bestFit = candidate;
				currentVariance = candidateVariance;
				// We adjust the mutation rate, increasing it if we get closer
				mutation *= 1.1;
			} else {
				/// ...and decreasing if we don't
				mutation /= 1.1;
			}
		}
		previousChange = lastChange;
		lastChange = Math.abs(oldBestGuess-bestFit.center);
		current = true;
	}

	/**
	 * Inform the ParameterOptomizer about a new sample, the results of
	 * a test with a particular input value.
	 * @param input The input value
	 * @param output The resulting "score" or performance, lower indicates better
	 */
	public void addSample(double input, double output) {
		samples.put(input, output);
		current = false;
	}
}

class Curve {
	double width, center, height;

	public Curve() {
		this(0, 1, 1);
	}

	/**
	 * Create a new curve
	 * @param width Indicates the width or steepness of the curve's slopes
	 * @param center The position of the center or lowest point of the curve
	 * @param height The height of the center of the curve
	 */
	public Curve(double width, double center, double height) {
		this.width = width;
		this.center = center;
		this.height = height;
	}

	/**
	 * Create a new curve from a template curve, including some randomness
	 * @param template The template curve
	 * @param mutation The amount of randomness
	 * @param min The minimum appropriate value for the center of the curve
	 * @param max The maximum appropriate value for the center of the curve
	 */
	public Curve(Curve template, double mutation, double min, double max) {
		this.width = template.width + getPeturb(mutation);
		if (width < 0)
			width = 0;
		this.center = template.center + getPeturb(mutation);
		if (center < min)
			center = min;
		if (center > max)
			center = max;
		this.height = template.height + getPeturb(mutation);
		if (height < 0)
			height = 0;
	}

	private double getPeturb(double mutation) {
		return ((2.0 * ParameterOptimizer.random.nextDouble()) - 1.0) * mutation;
	}
	
	/**
	 * Get the height of the curve for a particular value of X
	 */
	public double getYAt(double x) {
		return width * Math.pow(center - x, 2) + height;
	}

	/**
	 * Get the variance of this curve from a set of samples
	 * @return The variance, lower means a closer fit
	 */
	public double getVariance(Map<Double, Double> samples) {
		double variance = 0;
		for (Map.Entry<Double, Double> e : samples.entrySet()) {
			variance += Math.pow(e.getValue() - getYAt(e.getKey()), 2);
		}
		return variance / (double) samples.size();
	}

	public String toString() {
		return "W: " + width + " C:" + center + " H:" + height;
	}
}